use std::collections::HashMap;
use std::time::Duration;

use rand::seq::SliceRandom;
use reqwest::{blocking, Client, header::{
    ACCEPT, ACCEPT_LANGUAGE, CONNECTION, HeaderMap, USER_AGENT,
}, Method, Response, Url};
use reqwest::header::{HeaderName, HeaderValue};

const UA_LIST: [&str; 8] = [
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.71",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)",
    "Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.50",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0",
];

fn random_user_agent() -> String { UA_LIST.choose(&mut rand::thread_rng()).unwrap().to_string() }

fn default_headers() -> HeaderMap {
    let mut headers = HeaderMap::new();
    headers.insert(
        USER_AGENT,
        random_user_agent()
            .parse()
            .expect("Parse user agent to header value failed"),
    );
    headers.insert(
        ACCEPT,
        "*/*".parse().expect("Parse accept to header value failed"),
    );
    headers.insert(
        CONNECTION,
        "keep-alive"
            .parse()
            .expect("Parse connection to header value failed"),
    );
    headers.insert(
        ACCEPT_LANGUAGE,
        "zh-CN,zh;q=0.8"
            .parse()
            .expect("Parse accept language to header value failed"),
    );
    headers
}

const REQUEST_DURATION: Duration = Duration::from_millis(0);

pub fn generate_random_ua() -> String {
    return UA_LIST
        .choose(&mut rand::thread_rng())
        .expect("Choose random UA failed")
        .to_string();
}

pub trait Credential {
    fn get_cookies(&self) -> HashMap<String, String>;
    fn get_cookie_string(&self) -> String;
    fn get_headers(&self) -> HeaderMap;
}

#[derive(Debug)]
pub struct Session<T: Credential> {
    client: blocking::Client,
    aclient: Client,
    pub credential: T,
}

impl<T: Credential> Session<T> {
    pub fn new(credential: T) -> Self {
        let client = blocking::Client::builder()
            .default_headers(default_headers())
            .build()
            .expect("Build client failed");
        let aclient = Client::builder()
            .default_headers(default_headers())
            .build()
            .expect("Build async client failed");

        Self {
            client,
            aclient,
            credential,
        }
    }

    pub fn request(&self, url: Url, method: Method, headers: HeaderMap)
                   -> anyhow::Result<blocking::Response>
    {
        let headers = if headers.is_empty() {
            self.credential.get_headers()
        } else {
            headers
        };

        std::thread::sleep(REQUEST_DURATION);
        Ok(self.client
            .request(method, url)
            .headers(headers)
            .send()?)
    }

    pub async fn arequest(&self, url: Url, method: Method, headers: HeaderMap)
                          -> anyhow::Result<Response>
    {
        let headers = if headers.is_empty() {
            self.credential.get_headers()
        } else {
            headers
        };

        async_std::task::sleep(REQUEST_DURATION).await;
        Ok(self.aclient
            .request(method, url)
            .headers(headers)
            .send()
            .await?)
    }
}
